﻿using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Xml.Linq;
using Microsoft.SharePoint;
using Sapphire.Linq.FieldMaps;

namespace Sapphire.Linq.Framework
{
    public class CamlExpressionVisitor : ExpressionVisitor
    {
        private readonly IPropertyMapsContext _mapsContext;
        private uint _rowCount = int.MaxValue;
        private XElement _where = null;
        private XElement _orderBy = null;
        private uint _skipCount;

        private CamlExpressionVisitor(IPropertyMapsContext mapsContext)
        {
            _mapsContext = mapsContext;
        }

        public static CamlExpressionVisitor Create(IPropertyMapsContext mapsContext)
        {
            return new CamlExpressionVisitor(mapsContext);
        }

        public SPQuery ParseSkipQuery(Expression expression)
        {
            var resulQuery = Parse(expression);
            resulQuery.RowLimit = _skipCount;
            return resulQuery;
        }

        public SPQuery Parse(Expression expression)
        {
            Visit(expression);
            var query = new SPQuery();
            query.ExpandRecurrence = true;
            query.RowLimit = _rowCount;
            query.ViewFields = GenerateViewFields(TypesHelper.GetEntityType(expression));

            query.Query = _where.ToStringSafe() + _orderBy.ToStringSafe();

            return query;
        }

        private string GenerateViewFields(Type getEntityType)
        {
            return CAMLBuilder.Create(_mapsContext).ViewFields(getEntityType)
                .Select(x => x.ToStringSafe())
                .Aggregate(string.Concat);
        }

        protected override Expression VisitMethodCall(MethodCallExpression m)
        {
            switch (m.Method.Name)
            {
                case "First":
                case "FirstOrDefault":
                    SetWhereQuery(m);
                    _rowCount = 1;
                    break;

                case "OrderBy":
                case "OrderByDescending":
                    SetOrderBy(m);
                    break;

                case "Count":
                case "GroupBy":
                case "StartsWith":
                case "Contains":
                case "Where":
                    SetWhereQuery(m);
                    break;

                case "Skip":
                    _skipCount = Convert.ToUInt32(((ConstantExpression)m.Arguments[1]).Value);
                    break;

                case "Take":
                    _rowCount = Convert.ToUInt32(((ConstantExpression)m.Arguments[1]).Value);
                    break;

                case "Max":
                case "Min":
                    throw new NotSupportedException();

                default:
                    break;
            }

            return base.VisitMethodCall(m);
        }

        private void SetOrderBy(MethodCallExpression m)
        {
            if (m.Arguments.Count == 1)
                return;

            Expression arg = m.Arguments[1];
            var unExpr = arg as UnaryExpression;
            var op = unExpr.Operand as LambdaExpression;

            _orderBy = _orderBy ?? new XElement("OrderBy");

            _orderBy.AddFirst(CAMLBuilder.Create(_mapsContext).OrderBy(m, op));
        }

        private void SetWhereQuery(MethodCallExpression m)
        {
            if (m.Arguments.Count == 1)
                return;

            Expression arg = m.Arguments[1];
            var unExpr = arg as UnaryExpression;
            var op = unExpr.Operand as LambdaExpression;

            _where = _where ?? new XElement("Where");

            _where.AddFirst(CreateCamlOperand(op.Body));
        }

        private XElement CreateCamlOperand(Expression expression)
        {
            var binary = expression as BinaryExpression;

            if (binary != null)
            {
                return CreateCamlOperand(binary);
            }

            var methodCall = expression as MethodCallExpression;

            if (methodCall != null)
            {
                return CreateCamlOperand(methodCall);
            }

            throw new NotImplementedException();
        }

        private XElement CreateCamlOperand(MethodInfo method, MemberExpression member)
        {
            switch (method.Name)
            {
                case "GroupBy":
                    return CAMLBuilder.Create(_mapsContext).DynamicUnary("GroupBy", member.Member);

                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        private XElement CreateCamlOperand(MethodCallExpression methodCall)
        {
            switch (methodCall.Method.Name)
            {
                case "StartsWith":
                    return CAMLBuilder.Create(_mapsContext).DynamicUnary("BeginsWith", methodCall.Object as MemberExpression, methodCall.Arguments[0]);
                case "Contains":
                    return CAMLBuilder.Create(_mapsContext).DynamicUnary("Contains", methodCall.Object as MemberExpression, methodCall.Arguments[0]);

                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        private XElement CreateCamlOperand(BinaryExpression body)
        {
            switch (body.NodeType)
            {
                case ExpressionType.GreaterThan:
                case ExpressionType.GreaterThanOrEqual:
                case ExpressionType.LessThan:
                case ExpressionType.LessThanOrEqual:
                case ExpressionType.Equal:
                case ExpressionType.NotEqual:
                    return CAMLBuilder.Create(_mapsContext).DynamicUnary(body.NodeType, body.Left as MemberExpression, body.Right);
                case ExpressionType.And:
                case ExpressionType.AndAlso:
                    var elementAnd = new XElement("And");
                    elementAnd.AddFirst(CreateCamlOperand(body.Left));
                    elementAnd.AddFirst(CreateCamlOperand(body.Right));
                    return elementAnd;
                case ExpressionType.Or:
                case ExpressionType.OrElse:
                    var elementOr = new XElement("Or");
                    elementOr.AddFirst(CreateCamlOperand(body.Left));
                    elementOr.AddFirst(CreateCamlOperand(body.Right));
                    return elementOr;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }


    }
}